跳到主要内容

Go 语言学习-反射性能

转载自 极客兔兔-Go Reflect 提高反射性能

反射的性能

毫无疑问的是,反射会增加额外的代码指令,对性能肯定会产生影响的。具体影响有多大,我们可以使用 Benchmark 来测试一番。

创建对象

// 正常的方式创建对象
func BenchmarkNew(b *testing.B) {
var config *Config
for i := 0; i < b.N; i++ {
config = new(Config)
}
_ = config
}


// 反射的方式创建对象
func BenchmarkReflectNew(b *testing.B) {
var config *Config
typ := reflect.TypeOf(Config{})
b.ResetTimer()
for i := 0; i < b.N; i++ {
config, _ = reflect.New(typ).Interface().(*Config)
}
_ = config
}

测试结果如下:

$ go test -bench .          
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkNew-8 26478909 40.9 ns/op
BenchmarkReflectNew-8 18983700 62.1 ns/op
PASS
ok example/hpg-reflect 2.382s

通过反射创建对象的耗时约为 new 的 1.5 倍,相差不是特别大。

修改字段的值 ⭐

通过反射获取结构体的字段有两种方式,一种是 FieldByName,另一种是 Field(按照下标)。前面的例子中,我们使用的是 FieldByName。

// 原生的直接赋值
func BenchmarkSet(b *testing.B) {
config := new(Config)
b.ResetTimer()
for i := 0; i < b.N; i++ {
config.Name = "name"
config.IP = "ip"
config.URL = "url"
config.Timeout = "timeout"
}
}

// 通过反射下标来赋值
func BenchmarkReflect_FieldSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(0).SetString("name")
ins.Field(1).SetString("ip")
ins.Field(2).SetString("url")
ins.Field(3).SetString("timeout")
}
}

// 通过反射 FieldByName 来赋值
func BenchmarkReflect_FieldByNameSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.FieldByName("Name").SetString("name")
ins.FieldByName("IP").SetString("ip")
ins.FieldByName("URL").SetString("url")
ins.FieldByName("Timeout").SetString("timeout")
}
}

测试结果如下:

$ go test -bench="Set$" .          
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.302 ns/op
BenchmarkReflect_FieldSet-8 33913672 34.5 ns/op
BenchmarkReflect_FieldByNameSet-8 3775234 316 ns/op
PASS
ok example/hpg-reflect 3.066s

三种场景下,对象已经提前创建好,测试的均为给字段赋值所消耗的时间。

  • 普通的赋值操作,每次耗时约为 0.3 ns,
  • 通过下标找到对应的字段再赋值,每次耗时约为 30 ns,
  • 通过名称找到对应字段再赋值,每次耗时约为 300 ns。

总结一下,对于一个普通的拥有 4 个字段的结构体 Config 来说,使用反射给每个字段赋值,相比直接赋值,性能劣化约 100 - 1000 倍。其中,FieldByName 的性能相比 Field 劣化 10 倍。

FieldByName 和 Field 性能差距

打开源代码一探究竟:

reflect/value.go

// FieldByName returns the struct field with the given name.
// It returns the zero Value if no field was found.
// It panics if v's Kind is not struct.
func (v Value) FieldByName(name string) Value {
v.mustBe(Struct)
if f, ok := v.typ.FieldByName(name); ok {
return v.FieldByIndex(f.Index)
}
return Value{}
}

reflect/type.go

func (t *rtype) FieldByName(name string) (StructField, bool) {
if t.Kind() != Struct {
panic("reflect: FieldByName of non-struct type")
}
tt := (*structType)(unsafe.Pointer(t))
return tt.FieldByName(name)
}

// FieldByName returns the struct field with the given name
// and a boolean to indicate if the field was found.
func (t *structType) FieldByName(name string) (f StructField, present bool) {
// Quick check for top-level name, or struct without embedded fields.
hasEmbeds := false
if name != "" {
for i := range t.fields {
tf := &t.fields[i]
if tf.name.name() == name {
return t.Field(i), true
}
if tf.embedded() {
hasEmbeds = true
}
}
}
if !hasEmbeds {
return
}
return t.FieldByNameFunc(func(s string) bool { return s == name })
}

整个调用链条是比较简单的:

(v Value) FieldByName -> (t *rtype) FieldByName -> (t *structType) FieldByName

(t *structType) FieldByName 中使用 for 循环,逐个字段查找,字段名匹配时返回。也就是说,在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1),而按照 Name 访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大。

如何提高性能

避免使用反射

使用反射赋值,效率非常低下,如果有替代方案,尽可能避免使用反射,特别是会被反复调用的热点代码。

例如 RPC 协议中,需要对结构体进行序列化和反序列化,这个时候避免使用 Go 语言自带的 json 的 Marshal 和 Unmarshal 方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。可选的替代方案有 easyjson,在大部分场景下,相比标准库,有 5 倍左右的性能提升。

缓存 Index

在上面的例子中可以看到,FieldByName 相比于 Field 有一个数量级的性能劣化。

那在实际的应用中,就要避免直接调用 FieldByName。我们可以 利用字典将 Name 和 Index 的映射缓存起来。避免每次反复查找,耗费大量的时间。

利用缓存,优化下刚才的测试用例:

func BenchmarkReflect_FieldByNameCacheSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
cache := make(map[string]int)
for i := 0; i < typ.NumField(); i++ {
cache[typ.Field(i).Name] = i
}
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(cache["Name"]).SetString("name")
ins.Field(cache["IP"]).SetString("ip")
ins.Field(cache["URL"]).SetString("url")
ins.Field(cache["Timeout"]).SetString("timeout")
}
}

测试结果如下:

$ go test -bench="Set$" . -v
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.303 ns/op
BenchmarkReflect_FieldSet-8 33429990 34.1 ns/op
BenchmarkReflect_FieldByNameSet-8 3612130 331 ns/op
BenchmarkReflect_FieldByNameCacheSet-8 14575906 78.2 ns/op
PASS
ok example/hpg-reflect 4.280s

消耗时间从原来的 10 倍,缩小到了 2 倍。